//
//  SketchUpdateController.m
//  RunSketch
//
//  Created by Brian Hinch on 7/27/08.
//  Copyright 2008 Tellart. All rights reserved.
//

#import "SketchUpdateController.h"
#import "RootViewController.h"

@implementation SketchUpdateController

- (void)setWatcher:(RootViewController *)w {
    watcher = w;
}

+ (id) getSketchEntryPointURL {
    //determine the path the to the index.html file in the Resources directory
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *filePathString = [NSString stringWithFormat:@"%@/%@/%@.%@", [paths objectAtIndex:0], kLiveSketchDirectoryName, kSketchIndexFileName, kSketchIndexFileType];
    NSFileManager *fileman = [NSFileManager defaultManager];
    
    if ([fileman fileExistsAtPath:filePathString] == NO) {
        filePathString = [[NSBundle mainBundle]
            pathForResource:kSketchIndexFileName
            ofType:kSketchIndexFileType
            inDirectory:kLiveSketchDirectoryName
        ];
    }
    //return the URL and the request for the index.html file
    return [NSURL fileURLWithPath:filePathString];
}

/*
 * initiates a "live" update of the running sketch from an HTTP server
 * The server should respond with an XML file that describes all of the files/folders in the sketch.
 * RunSketch will then go through each entry in this file and start a new HTTP GET request
 * for each file. The results of these requests are saved to a temporary folder on
 * the mobile device. When each entry in the XML file has been downloaded this temporary
 * folder is swapped in for the real sketch folder and the WebView is reloaded.
 *
 */
- (void)updateFromURL:(NSURL *)url {
    //flag
    updating = YES;

    downloadRelativePathsQue = [NSMutableArray array];
    [downloadRelativePathsQue retain];
    
    NSURLRequest *theRequest=[NSURLRequest requestWithURL:url
        cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData
        timeoutInterval:kURLTimeoutInterval];
    
    //store for later use
    rootURL = url;
    
    [rootURL retain];

    if (theConnection) [theConnection release];
    theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
    if (theConnection) {
        // Create the NSMutableData that will hold
        // the received data
        receivedData=[[NSMutableData data] retain];
    } else {
        // deal with this event
        [self event_didFailWithConnectionError];
        //[theConnection release];
    }
    //[theRequest release];
}

/*
 * stops any update already in progress
 *
 */
- (void)stop {
    NSLog(@"SketchUpdate: stop called...");
    
    if (theConnection) {
        [theConnection cancel];
        NSLog(@"SketchUpdate: NSURLConnection cancelled.");
        //[theConnection release];
        //NSLog(@"SketchUpdate: NSURLConnection released.");
        [receivedData setLength:0];
        NSLog(@"SketchUpdate: receivedData zeroed.");
    }
    downloading = NO;
    //prepare the filesystem for future updates
    [self prepareFilesystem];

    [self event_didStop];
}

/*** urlconnection methods ******************************************************/

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    // this method is called when the server has determined that it
    // has enough information to create the NSURLResponse
 
    // it can be called multiple times, for example in the case of a
    // redirect, so each time we reset the data.
    // receivedData is declared as a method instance elsewhere
    [receivedData setLength:0];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    // append the new data to the receivedData
    // receivedData is declared as a method instance elsewhere
    [receivedData appendData:data];

    sketchBytesLoaded += [data length];

    // deal with this event
    [self event_madeProgress:sketchBytesLoaded:sketchBytesTotal];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    // release the connection, and the data object
    [connection release];
    theConnection = nil;
    // receivedData is declared as a method instance elsewhere
    [receivedData release];
 
    // inform the user
    [self event_didFailWithCommunicationError:
        [NSString stringWithFormat:@"ERROR: SketchUpdatecontroller connection failed! Error - %@ %@",
          [error localizedDescription],
          [[error userInfo] objectForKey:NSErrorFailingURLStringKey]]];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    if (downloading == YES) {
        [self didFinishLoadingFile:connection];
    } else {
        [self didFinishLoadingCatalog:connection];
    }
}

- (void)didFinishLoadingFile:(NSURLConnection *)connection {
    NSLog([NSString stringWithFormat:@"SketchUpdateController: didFinishLoadingFile: Donwloaded file %i", currentDownloadIndex]);

    // release the connection
    [connection release];
    theConnection = nil;
 
    // do something with the data
    NSLog(@"didFinishLoadingFile");

    NSString *sRelativePath = [downloadRelativePathsQue objectAtIndex:currentDownloadIndex];
    NSString *sAbsolutePath = [sUpdatingSketchBasePath stringByAppendingString:sRelativePath];
    NSError *fsErr;
    
    //increment the download index
    currentDownloadIndex ++;
    
    NSLog([NSString stringWithFormat:@"SketchUpdateController: didFinishLoadingFile: saving to %@", sAbsolutePath]);
    //save the data to disk
    BOOL success = [receivedData writeToFile: sAbsolutePath options: 0 error: &fsErr];

    // release the data object
    [receivedData release];

    //set flag
    downloading = NO;
    
    if (success == NO) {
        //error saving file
        //[self event_didFailWithCommunicationError: [NSString stringWithFormat:@"SketchUpdateController: didFinishLoadingFile: ERROR creating saving downloaded file %@ : %@", sAbsolutePath, [fsErr localizedDescription]]];
		NSLog([NSString stringWithFormat:@"SketchUpdateController: didFinishLoadingFile: ERROR creating saving downloaded file, skipping %@ : %@", sAbsolutePath, [fsErr localizedDescription]]);
        return;
    }
}
- (void)didFinishLoadingCatalog:(NSURLConnection *)connection {
    // receivedData is declared as a method instance elsewhere
    NSLog(@"SketchUpdateController: didFinishLoadingFile: Succeeded! Received %d bytes of data",[receivedData length]);
    // release the connection, and the data object
    [connection release];
    theConnection = nil;

    // notify the watcher
    [self event_didLoadCatalog];

    // prepare the filesystem
    if ([self prepareFilesystem] == NO) {
        // could not prepare the filesystem, so do not continue with the update
        // notify the watcher
        [self event_didFailWithConnectionError];
        return;
    }
    
    //zero the byte counter
    sketchBytesLoaded = 0;
    sketchBytesTotal = 0;
    
    // construct an NSXMLParser to examine the catalog
    NSXMLParser *parser = [[NSXMLParser alloc] initWithData:receivedData];

    // Set self as the delegate of the parser so that it will receive the parser delegate methods callbacks.
    [parser setDelegate:self];
    // Depending on the XML document you're parsing, you may want to enable these features of NSXMLParser.
    [parser setShouldProcessNamespaces:NO];
    [parser setShouldReportNamespacePrefixes:NO];
    [parser setShouldResolveExternalEntities:NO];
    
    totalNumDownloads = 0;

    // start parsing the catalog
    [parser parse];
    
    NSError *parseError = [parser parserError];
    if (parseError) {
        // notify the watcher of an error parsing the catalog
        [self event_didFailWithCatalogParserError:parseError];
        
        // release the parser object
        [parser release];
        
        return;
    }
    
    // release the parser object
    [parser release];

    // release the data object
    [receivedData release];

    //start the downloads
    [self startDownloads];
}

- (void) startDownloads {
    //prepare for downloading files
    currentDownloadIndex = 0;
    downloading = NO;
    
    //schedule a timer to call downloadFromTimer
    [NSTimer scheduledTimerWithTimeInterval: kDownloadTimerInterval
        target: self
        selector: @selector(downloadFromTimer:)
        userInfo: nil
        repeats: YES];
}

- (BOOL)prepareFilesystem {
    //we'll need a file manager for this
    NSFileManager *fileman = [NSFileManager defaultManager];
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSError *fsErr;

    //get the "live" sketch base path
    sLiveSketchBasePath = [NSString stringWithFormat:@"%@/%@", [paths objectAtIndex:0], kLiveSketchDirectoryName];
    [sLiveSketchBasePath retain];
    
    //make sure it exists
    if ([fileman fileExistsAtPath:sLiveSketchBasePath] == NO) {
        //create the sketch directory
        if ([fileman createDirectoryAtPath:sLiveSketchBasePath withIntermediateDirectories:YES attributes:nil error:&fsErr] == NO) {
            //error creating directory
            NSLog([NSString stringWithFormat:@"SketchUpdateController: prepareFilesystem: ERROR creating live sketch dir: %@", [fsErr localizedDescription]]);
            return NO;
        }
    }
    
    //get the "updating" sketch base path
    sUpdatingSketchBasePath = [NSString stringWithFormat:@"%@/%@", [paths objectAtIndex:0], kUpdatingSketchDirectoryName];
    [sUpdatingSketchBasePath retain];
    
    //if it exists, remove it
    if ([fileman fileExistsAtPath:sUpdatingSketchBasePath] == YES) {
        NSLog([NSString stringWithFormat:@"SketchUpdateController: prepareFilesystem: will remove %@", sUpdatingSketchBasePath]);
        if ([fileman removeItemAtPath:sUpdatingSketchBasePath error:&fsErr] == NO) {
            //error removing directory
            NSLog([NSString stringWithFormat:@"SketchUpdateController: prepareFilesystem: ERROR removing updating sketch dir: %@", [fsErr localizedDescription]]);
            return NO;
        }
    }
    
    //and create it again
    if ([fileman fileExistsAtPath:sUpdatingSketchBasePath] == NO) {
        //create the sketch directory
        if ([fileman createDirectoryAtPath:sUpdatingSketchBasePath withIntermediateDirectories:YES attributes:nil error:&fsErr] == NO) {
            //error creating directory
            NSLog([NSString stringWithFormat:@"SketchUpdateController: prepareFilesystem: ERROR creating updating sketch dir: %@", [fsErr localizedDescription]]);
            return NO;
        }
    }
    
    //cleanup
    /*
    [fileman release];
    [paths release];
    [fsErr release];
    */
    
    return YES;
}

/*** xml parsing methods ******************************************************************/

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
    NSScanner *strToInt;
    NSString *sRelativePath;
    NSString *sAbsolutePath;
    NSString *sFileSize;
    NSString *sSketchBytesTotal;
    int iFileSize = 0;
    
    /*
     * <kCAT_EL_ASSET> element
     */
    if ([elementName isEqualToString:kCAT_EL_ASSET]) {
        //the asset's relative path is stored in its kCAT_ATR_SRC attribute
        sRelativePath = [NSString stringWithFormat:@"%@", [attributeDict valueForKey:kCAT_ATR_SRC]];
        sFileSize = [NSString stringWithFormat:@"%@", [attributeDict valueForKey:kCAT_ATR_SIZE]];
        if (sFileSize) {
            strToInt = [NSScanner scannerWithString:sFileSize];
            if ([strToInt scanInt:&iFileSize] == NO) iFileSize = 0;
        } else {
            iFileSize = 0;
        }
        
        /*
         * kASSET_TYPE_FILE elements
         */ 
        if ([[attributeDict valueForKey:kCAT_ATR_TYPE] isEqualToString:kASSET_TYPE_FILE]) {
            NSLog([NSString stringWithFormat:@"SketchUpdateController: catalog file (%i bytes) - %@", iFileSize, sRelativePath]);
            [downloadRelativePathsQue addObject:sRelativePath];
            
            //this seems to be a bit faster than [downloadRelativePathsQue count]
            totalNumDownloads++;

        /*
         * kASSET_TYPE_DIR elements
         */ 
        } else if ([[attributeDict valueForKey:kCAT_ATR_TYPE] isEqualToString:kASSET_TYPE_DIR]) {
            NSLog([NSString stringWithFormat:@"SketchUpdateController: catalog dir - %@", sRelativePath]);
            //create the directory in the updating sketch base dir
            sAbsolutePath = [sUpdatingSketchBasePath stringByAppendingString:sRelativePath];
            if ([[NSFileManager defaultManager] createDirectoryAtPath:sAbsolutePath attributes:nil] == NO) {
                NSLog([NSString stringWithFormat:@"SkethcUpdateController: ERROR creating local path %@", sAbsolutePath]);
            }
        }
    
    /*
     * <kCAT_EL_CATALOG> element
     */
    } else if ([elementName isEqualToString:kCAT_EL_CATALOG]) {
        //record the kCAT_ATR_BASE attribute in an instance var
        baseURL = [NSString stringWithFormat:@"%@", [attributeDict valueForKey:kCAT_ATR_BASE]];
        [baseURL retain];
        sSketchBytesTotal = [NSString stringWithFormat:@"%@", [attributeDict valueForKey:kCAT_ATR_SIZE]];
        if (sFileSize) {
            strToInt = [NSScanner scannerWithString:sSketchBytesTotal];
            if ([strToInt scanInt:&sketchBytesTotal] == NO) sketchBytesTotal = 0;
        } else {
            sketchBytesTotal = 0;
        }
        
    }
    
    //record the last element for the next time this method is called
    lastElementName = elementName;
    
    //cleanup
    /*
    [strToInt release];
    [sRelativePath release];
    [sAbsolutePath release];
    [sRelativeURL release];
    [sFileSize release];
    [sSketchBytesTotal release];
    */
}

/* start the next download */
- (void)downloadFromTimer:(NSTimer *)theTimer {
    NSLog([NSString stringWithFormat:@"*TIMER* SketchUpdateController: downloadFromTimer"]);

    //don't start the next download if we're no longer updating
    if (updating == NO) {
        //no more future downloads either, stop the timer
        [theTimer invalidate];
        // all done
        return;
    }

    //don't start the next download if one is already in progress
    if (downloading == YES) return;
    
    //check if there are actually any more downloads to do
    if (currentDownloadIndex >= totalNumDownloads) {
        //All downloads are complete, stop the timer
        [theTimer invalidate];
        //move the updated sketch into the live position
        [self replaceLiveSketchWithUpdatingSketch];
        // notify the watcher
        [self event_didSucceed];
        // all done
        return;
    }

    NSLog([NSString stringWithFormat:@"SketchUpdateController: downloadFromTimer: Starting download %i (there are %i downloads total).", currentDownloadIndex, totalNumDownloads]);

    //get the URL and paths of the next thing to download
    NSString *sRelativePath = [downloadRelativePathsQue objectAtIndex:currentDownloadIndex];
    NSString *sRelativeURL = [NSString stringWithFormat:@"%@%@", baseURL, sRelativePath];
    
    //create a request for the file's URL (relative to rootURL)
    NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:sRelativeURL relativeToURL:rootURL]
        cachePolicy:NSURLRequestUseProtocolCachePolicy
        timeoutInterval:kURLTimeoutInterval];
        
    //create a urlconnection from the request
    if (theConnection) [theConnection release];
    theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
    if (theConnection) {
        //attempt to download the file to sAbsolutePath
        receivedData=[[NSMutableData data] retain];
        downloading = YES;
        
    } else {
        //could not start a download!
        NSLog([NSString stringWithFormat:@"SkethcUpdateController: ERROR could not start download of %@", sRelativeURL]);

        //Failure, stop the timer
        [theTimer invalidate];
        
        // deal with this event
        [self event_didFailWithConnectionError];
        
        // all done
        return;
    }
}

-(BOOL)replaceLiveSketchWithUpdatingSketch {
    //we'll need a file manager for this
    NSFileManager *fileman = [NSFileManager defaultManager];
    NSError *fsErr;

    //if it exists, remove the live sketch
    if ([fileman fileExistsAtPath:sLiveSketchBasePath] == YES) {
        NSLog([NSString stringWithFormat:@"SketchUpdateController: replaceLiveSketchWithUpdatingSketch: will remove %@", sLiveSketchBasePath]);
        if ([fileman removeItemAtPath:sLiveSketchBasePath error:&fsErr] == NO) {
            //error removing directory
            NSLog([NSString stringWithFormat:@"SketchUpdateController: replaceLiveSketchWithUpdatingSketch: ERROR removing live sketch dir: %@", [fsErr localizedDescription]]);
            return NO;
        }
    }
    
    //if it exists, move the updating sketch to the live sketch
    if ([fileman fileExistsAtPath:sUpdatingSketchBasePath] == YES) {
        if ([fileman moveItemAtPath: sUpdatingSketchBasePath
            toPath:sLiveSketchBasePath
            error:&fsErr] == NO) {
            //error removing directory
            NSLog([NSString stringWithFormat:@"SketchUpdateController: replaceLiveSketchWithUpdatingSketch: ERROR moving updating sketch dir to live sketch dir: %@", [fsErr localizedDescription]]);
            return NO;
        }
    }
    
    return YES;
    
}


/*** event processing methods below here *********************************************************/

-(void) event_didFailWithConnectionError {
    //set flags
    updating = NO;
    downloading = NO;
    [watcher didFailWithConnectionError:self];
}
-(void) event_didFailWithCommunicationError:(NSString *)msg {
    //set flags
    updating = NO;
    downloading = NO;
    [watcher didFailWithCommunicationError:self:msg];
}
- (void) event_didFailWithCatalogParserError:(NSError *)err {
    //set flags
    updating = NO;
    downloading = NO;
    [watcher didFailWithCatalogParserError:self:err];
}
- (void) event_didLoadCatalog {
    [watcher didLoadCatalog:self];
}
-(void) event_madeProgress: (int)bytesLoaded: (int)bytesTotal {
    [watcher madeProgress:self: bytesLoaded: bytesTotal];
}
-(void) event_didStop {
    //set flags
    updating = NO;
    downloading = NO;
    [watcher didStop:self];
}
-(void) event_didSucceed {
    //set flags
    updating = NO;
    downloading = NO;
    [watcher didSucceed:self];
}

/***********************************************************************/

- (void)dealloc {
    [watcher release];
    [downloadRelativePathsQue release];
    [sLiveSketchBasePath release];
    [sUpdatingSketchBasePath release];
    [baseURL release];
	[super dealloc];
}

@end
